########################################################################
#  Searx-Qt - Lightweight desktop application for Searx.
#  Copyright (C) 2020-2022  CYBERDEViL
#
#  This file is part of Searx-Qt.
#
#  Searx-Qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-Qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

from PyQt5.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QTableView,
    QHeaderView,
    QAbstractItemView,
    QHBoxLayout,
    QFormLayout,
    QCheckBox,
    QMessageBox,
    QMenu,
    QComboBox,
    QSizePolicy,
    QScrollArea,
    QSplitter,
    QFrame,
    QWidgetAction
)

from PyQt5.QtCore import pyqtSignal, Qt, pyqtProperty

from PyQt5.QtGui import QGuiApplication, QPainter, QFontMetrics, QColor

import json

from searxqt.core.handler import NetworkTypes  # for network type filtering.
from searxqt.core.requests import ErrorType

from searxqt.widgets.buttons import Button
from searxqt.widgets.dialogs import UrlDialog, RequestsErrorMessage

from searxqt.models.instances import (
    InstanceTableModel,
    InstancesModelTypes
)

from searxqt.translations import _, timeToString


class AddUserInstanceDialog(UrlDialog):
    def __init__(self, url='', parent=None):
        UrlDialog.__init__(self, url=url, acceptTxt=_("Add"), parent=parent)

        layout = self.layout()

        label = QLabel("Update data on add:")
        self._updateCheckBox = QCheckBox("", self)

        layout.insertRow(2, label, self._updateCheckBox)

    def updateOnAdd(self):
        return bool(self._updateCheckBox.isChecked())


class InstancesView(QWidget):
    def __init__(self, filterModel, instanceSelecter, parent=None):
        """
        @type model: searxqt.models.instances.InstanceModelFilter
        @type instanceSelecter: searxqt.models.instances.InstanceSelecterModel
        @type parent: QObject
        """
        QWidget.__init__(self, parent=parent)
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._filterModel = filterModel
        self._instanceSelecter = instanceSelecter
        self._tableModel = InstanceTableModel(filterModel, self)

        # Diffrent types can have diffrent attibutes.
        self._currentInstancesType = filterModel.parentModel().Type

        headLabel = QLabel(f"<h2>{_('Instances')}</h2>", self)
        layout.addWidget(headLabel)

        # Splitter
        self._splitter = QSplitter(self)
        self._splitter.setOrientation(Qt.Vertical)
        layout.addWidget(self._splitter)

        # Filter scroll area
        self._scrollArea = QScrollArea(self._splitter)
        self._scrollContentWidget = QWidget(self._scrollArea)
        scrollLayout = QVBoxLayout(self._scrollContentWidget)

        self.filterWidget = FilterWidget(
            filterModel,
            self._scrollContentWidget
        )
        self._scrollArea.setWidget(self._scrollContentWidget)
        self._scrollArea.setFrameShape(QFrame.NoFrame)
        self._scrollArea.setWidgetResizable(True)
        self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._scrollArea.setAlignment(
            Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
        )
        scrollLayout.addWidget(self.filterWidget, 0, Qt.AlignTop)

        # Bottom widgets
        bottomWidget = QWidget(self._splitter)
        bottomLayout = QVBoxLayout(bottomWidget)

        self.statsWidget = InstancesStatsWidget(filterModel, bottomWidget)
        bottomLayout.addWidget(self.statsWidget)

        self._typeDependantButton = Button("", bottomWidget)
        bottomLayout.addWidget(self._typeDependantButton)

        self._tableView = InstanceTableView(
            filterModel,
            self._tableModel,
            self
        )
        bottomLayout.addWidget(self._tableView)

        # Connections
        self._tableView.selectionModel().selectionChanged.connect(
                                                self.__selectionChanged)
        self._tableModel.layoutChanged.connect(self.__tableModelLayoutChanged)
        self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)
        filterModel.parentModel().typeChanged.connect(
            self.__instancesModelTypeChanged
        )

    """ InstancesType dependant methods below
    """

    def __instancesModelTypeChanged(self, type_):
        """ PersistentInstancesModel emitted changed.
        """
        previousType = self._currentInstancesType

        # Disconnect signals
        if previousType == InstancesModelTypes.Stats2:
            self._typeDependantButton.clicked.disconnect(self.__updateClicked)
        elif previousType == InstancesModelTypes.User:
            self._typeDependantButton.clicked.disconnect(self.__addClicked)

        if type_ == InstancesModelTypes.Stats2:
            self._typeDependantButton.setText(_("Update"))
            self._typeDependantButton.clicked.connect(self.__updateClicked)
        elif type_ == InstancesModelTypes.User:
            self._typeDependantButton.setText(_("Add Instance"))
            self._typeDependantButton.clicked.connect(self.__addClicked)

        self._currentInstancesType = type_

    # InstancesModelTypes.Stats2
    def __updateClicked(self, checked):
        self._typeDependantButton.setEnabled(False)
        handler = self._filterModel.parentModel().handler()
        if handler.updateInstances():  # True when thread has started.
            handler.updateFinished.connect(self.__updateFinished)

    def __updateFinished(self, errorType, errorMsg):
        handler = self._filterModel.parentModel().handler()
        handler.updateFinished.disconnect(self.__updateFinished)

        if errorType != ErrorType.Success:
            dialog = RequestsErrorMessage(errorType, errorMsg)
            dialog.exec()

        self._typeDependantButton.setEnabled(True)

    # InstancesModelTypes.User
    def __addClicked(self, checked):
        dialog = AddUserInstanceDialog()
        if dialog.exec():
            handler = self._filterModel.parentModel().handler()
            handler.addInstance(dialog.url)
            if dialog.updateOnAdd():
                handler.updateInstance(dialog.url)

    """ -----------------------------------------------------------
    """

    def selectUrl(self, url):
        self._tableView.selectionModel().selectionChanged.disconnect(
                                                self.__selectionChanged)

        # Backup horizontal scroll value
        horScrollVal = self._tableView.horizontalScrollBar().value()

        if url in self._filterModel:
            rowIndex = self._tableModel.getByUrl(url)
            modelIndex = self._tableModel.index(rowIndex, 0)
            self._tableView.setCurrentIndex(modelIndex)
            self._tableView.scrollTo(
                modelIndex,
                QAbstractItemView.PositionAtCenter
            )
        else:
            # No URL set so clear the selection.
            self._tableView.clearSelection()

        # Restore horizontal scroll value
        self._tableView.horizontalScrollBar().setValue(horScrollVal)

        self._tableView.selectionModel().selectionChanged.connect(
                                                self.__selectionChanged)

    def __instanceChanged(self, url):
        """ Select and scroll to table item with passed url.

        @param url: Instance url to select and scroll to.
        @type url: str
        """
        if url in self._filterModel:
            rowIndex = self._tableModel.getByUrl(url)
            modelIndex = self._tableModel.index(rowIndex, 0)
            self._tableView.setCurrentIndex(modelIndex)
            self._tableView.scrollTo(
                modelIndex,
                QAbstractItemView.PositionAtCenter
            )

    def __tableModelLayoutChanged(self):
        """ Re-select the current URL when the layout changed.
        """
        self.selectUrl(self._instanceSelecter.currentUrl)

    def __selectionChanged(self):
        """ Let the InstanceSelecter know that the current instance has
        changed. (Selected by the user.)
        """
        self._instanceSelecter.instanceChanged.disconnect(
                                                self.__instanceChanged)

        selectionModel = self._tableView.selectionModel()
        if selectionModel.hasSelection():
            rows = selectionModel.selectedRows()
            selectedIndex = rows[0].row()

            self._instanceSelecter.currentUrl = (
                self._tableModel.getByIndex(selectedIndex)
            )
        else:
            self._instanceSelecter.currentUrl = ""

        self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)

    def saveSettings(self):
        header = self._tableView.horizontalHeader()

        data = {
            'hiddenColumnIndexes': self._tableView.getHiddenColumnIndexes(),
            'headerState': header.saveState(),
            'splitterState': self._splitter.saveState()
        }

        return data

    def loadSettings(self, data):
        self._tableView.setHiddenColumnIndexes(
            data.get('hiddenColumnIndexes', [])
        )

        if 'headerState' in data:
            header = self._tableView.horizontalHeader()
            header.restoreState(data['headerState'])

        if 'splitterState' in data:
            self._splitter.restoreState(data['splitterState'])


class InstanceTableView(QTableView):
    def __init__(self, filterModel, tableModel=None, parent=None):
        """
        @type filterModel: searxqt.models.instances.InstanceModelFilter
        @type tableModel: searxqt.models.instances.InstanceTableModel
        """
        QTableView.__init__(self, parent=parent)

        self._filterModel = filterModel

        if tableModel:
            self.setModel(tableModel)

        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.setSortingEnabled(True)
        self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)

        # Horizontal header
        header = self.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeToContents)
        header.setSectionsMovable(True)

    def _hideColumn(self, index, state):
        state = state == 0
        self.setColumnHidden(index, state)

    def getHiddenColumnIndexes(self):
        columnIndex = 0
        indexes = []
        for columnData in self.model().getColumns():

            # Set current value
            if self.isColumnHidden(columnIndex):
                indexes.append(columnIndex)

            columnIndex += 1
        return indexes

    def setHiddenColumnIndexes(self, indexes):
        for index in indexes:
            self._hideColumn(index, 0)

    """ Re-implementations
    """
    def contextMenuEvent(self, event):
        def __removeFromWhitelist(self, url):
            # Remove instance url from whitelist without applying the filter.
            self._filterModel.delInstanceFromWhitelist(url)

        selectionModel = self.selectionModel()

        if selectionModel.hasSelection():
            menu = QMenu(self)

            # Add to blacklist
            addToBlacklistAction = menu.addAction(_("Add to blacklist"))

            # Add to temporary blacklist
            addToTmpBlacklistAction = menu.addAction(_("Temporary blacklist"))

            # Add to whitelist
            addToWhitelistAction = menu.addAction(_("Add to whitelist"))

            menu.addSeparator()

            # Copy value(s) for any column
            copyMenu = QMenu(_("Copy column text"), self)
            copyMenuActions = []
            for columnData in self.model().getColumns():
                action = copyMenu.addAction(columnData.name)
                copyMenuActions.append(action)
            menu.addMenu(copyMenu)

            # Copy json values of selected instance(s)
            copyJson = menu.addAction(_("Copy JSON data"))
            menu.addSeparator()

            # Select all
            selectAllAction = menu.addAction(_("Select All"))
            menu.addSeparator()

            # Column options
            columnsMenu = QMenu(_("Columns"), self)
            menu.addMenu(columnsMenu)

            # User instances specific
            userInstances = False
            handler = self._filterModel.parentModel().handler()
            if (
                    self._filterModel.parentModel().Type
                    == InstancesModelTypes.User
            ):
                userInstances = True
                menu.addSeparator()
                delUserInstanceAction = menu.addAction(_("Remove selected"))
                updateUserInstanceAction = menu.addAction(_("Update selected"))

                if handler.hasActiveJobs():
                    delUserInstanceAction.setEnabled(False)

            columnIndex = 0
            for columnData in self.model().getColumns():
                action = QWidgetAction(columnsMenu)
                widget = QCheckBox(columnData.name, columnsMenu)
                widget.setTristate(False)
                action.setDefaultWidget(widget)

                # Set current value
                if not self.isColumnHidden(columnIndex):
                    widget.setChecked(True)

                columnsMenu.addAction(action)

                widget.stateChanged.connect(
                    lambda state, index=columnIndex:
                        self._hideColumn(index, state)
                )

                columnIndex += 1

            action = menu.exec_(self.mapToGlobal(event.pos()))

            if action == addToBlacklistAction:
                # Blacklist
                indexes = self.selectionModel().selectedRows()
                forAll = None
                for index in indexes:
                    url = self.model().getByIndex(index.row())
                    if url in self._filterModel.whitelist:
                        # Url is whitelisted, what to do?
                        if forAll == QMessageBox.YesRole:
                            __removeFromWhitelist(self, url)
                        elif forAll == QMessageBox.NoRole:
                            continue
                        else:
                            questionBox = QMessageBox(self)
                            questionBox.addButton(
                                _("Yes"), QMessageBox.YesRole
                            )
                            questionBox.addButton(
                                _("No"), QMessageBox.NoRole
                            )
                            questionBox.addButton(
                                _("Cancel"), QMessageBox.RejectRole
                            )
                            questionBox.setWindowTitle("Url whitelisted")
                            questionBox.setText(_(
                               f"{url} found in the <b>whitelist</b>. Would " \
                                "you like to <b>remove</b> it from the" \
                                " <b>whitelist</b> and add it to the" \
                                " <b>blacklist</b>?")
                            )

                            checkBox = QCheckBox(
                                _("Remember for all"), questionBox
                            )
                            questionBox.setCheckBox(checkBox)

                            questionBox.exec()
                            answer = questionBox.result()

                            if answer == 0:  # Yes
                                # Remove url from whitelist and add it to
                                # the blacklist
                                __removeFromWhitelist(self, url)

                                if checkBox.isChecked():
                                    forAll = QMessageBox.YesRole
                            elif answer == 1 and checkBox.isChecked():
                                forAll = QMessageBox.NoRole
                                continue
                            else:
                                continue

                    self._filterModel.putInstanceOnBlacklist(
                        url, reason=_("Manual")
                    )

                self._filterModel.apply()

            elif action == addToTmpBlacklistAction:
                index = selectionModel.selectedRows()[0]
                url = self.model().getByIndex(index.row())
                self._filterModel.putInstanceOnTimeout(url, reason=_("Manual"))
                self._filterModel.apply()

            elif action == addToWhitelistAction:
                # Whitelist
                indexes = self.selectionModel().selectedRows()
                for index in indexes:
                    url = self.model().getByIndex(index.row())
                    if url not in self._filterModel.whitelist:
                        self._filterModel.putInstanceOnWhitelist(url)
                self._filterModel.apply()

            elif action == selectAllAction:
                self.selectAll()

            # Copy a column data
            elif action in copyMenuActions:
                columnIndex = copyMenuActions.index(action)
                indexes = self.selectionModel().selectedRows()  # row indexes
                clipboard = QGuiApplication.clipboard()
                if len(indexes) > 1:
                    # Copy values from multiple rows
                    result = []
                    for index in indexes:
                        rowIndex = index.row()
                        newIndex = self.model().index(rowIndex, columnIndex)
                        data = self.model().data(newIndex, Qt.DisplayRole)
                        result.append(data)
                    clipboard.setText(str(result))
                else:
                    # Copy value from one row
                    rowIndex = indexes[0].row()  # row index
                    newIndex = self.model().index(rowIndex, columnIndex)
                    data = self.model().data(newIndex, Qt.DisplayRole)
                    clipboard.setText(str(data))

            # Copy JSON
            elif action == copyJson:
                indexes = self.selectionModel().selectedRows()  # row indexes
                instancesModel = self.model()._model
                result = {}

                for index in indexes:
                    url = self.model().data(index, Qt.DisplayRole)
                    instance = instancesModel[url]
                    result.update({instance.url: instance.data})

                clipboard = QGuiApplication.clipboard()
                clipboard.setText(json.dumps(result))

            # User instances actions
            if userInstances:
                if action == delUserInstanceAction:
                    indexes = self.selectionModel().selectedRows()

                    # Confirmation
                    instancesStr = ""
                    for index in indexes:
                        url = self.model().getByIndex(index.row())
                        instancesStr = f"{instancesStr}\n{url}"

                    confirmDialog = QMessageBox()
                    confirmDialog.setWindowTitle(
                        _("Delete instances")
                    )
                    confirmDialog.setText(
                        _("Are you sure you want to delete the following"
                          " instances?\n") + instancesStr
                    )
                    confirmDialog.setStandardButtons(
                        QMessageBox.Yes | QMessageBox.No
                    )
                    confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
                    confirmDialog.button(QMessageBox.No).setText(_("No"))

                    if confirmDialog.exec() != QMessageBox.Yes:
                        return

                    toRemove = []
                    for index in indexes:
                        toRemove.append(self.model().getByIndex(index.row()))

                    handler.removeMultiInstances(toRemove)

                elif action == updateUserInstanceAction:
                    indexes = self.selectionModel().selectedRows()
                    handler = self._filterModel.parentModel().handler()
                    for index in indexes:
                        handler.updateInstance(
                            self.model().getByIndex(index.row())
                        )


class UrlFilterLabel(QLabel):
    """ Fancy QLabel that eludes text and has a hover color property.
    """
    def __init__(self, text, parent):
        QLabel.__init__(self, text, parent=parent)
        self.__hover = False
        self.__hoverColor = QColor()
        self.setMinimumWidth(100)

    @pyqtProperty(QColor)
    def hoverColor(self):
        return self.__hoverColor

    @hoverColor.setter
    def hoverColor(self, color):
        self.__hoverColor = color

    def enterEvent(self, event):
        self.__hover = True
        self.update()

    def leaveEvent(self, event):
        self.__hover = False
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        if self.__hover and self.hoverColor.isValid():
            pen = painter.pen()
            pen.setColor(self.hoverColor)
            painter.setPen(pen)

        metrics = QFontMetrics(self.font())
        elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
        painter.drawText(self.rect(), self.alignment(), elided)


class UrlFilterItem(QWidget):
    deleted = pyqtSignal(str)  # url

    def __init__(self, url, parent=None):
        QWidget.__init__(self, parent=parent)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self._urlStr = url
        label = UrlFilterLabel(url, self)
        delButton = Button("X", self)
        delButton.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )

        layout.addWidget(label, 0, Qt.AlignTop)
        layout.addWidget(delButton, 0, Qt.AlignTop)

        delButton.clicked.connect(self.__delMe)

    def str(self): return self._urlStr

    def __delMe(self):
        self.deleted.emit(self.str())
        self.deleteLater()


class UrlFilterWidget(QWidget):
    def __init__(self, model, parent=None):
        QWidget.__init__(self, parent=parent)
        self._model = model
        self._items = {}

        QVBoxLayout(self)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)

        self._model.changed.connect(self.__modelChanged)

    # -- Override below in subclass.

    def filterItems(self):
        return []

    def delete(self, url):
        self._delete(url)

    def itemAdded(self, url):
        """ Callback from when a item has been added, usefull for subclasses
        to do something; for example set a tooltip.
        """
        pass

    # ---

    def __modelChanged(self):
        # Remove old
        for url in list(self._items.keys()):
            if url not in self.filterItems():
                self._items[url].deleteLater()
                del self._items[url]

        # Add new
        for url in self.filterItems():
            if url not in self._items:
                self._add(url)

    def _add(self, url):
        item = UrlFilterItem(url, self)
        self._items.update({url: item})
        item.deleted.connect(self.delete)
        self.layout().addWidget(item)
        self.itemAdded(url)

    def _delete(self, url):
        del self._items[url]


class WhitelistFilterWidget(UrlFilterWidget):
    def filterItems(self):
        return self._model.whitelist

    def delete(self, url):
        UrlFilterWidget.delete(self, url)
        self._model.delInstanceFromWhitelist(url)
        self._model.apply()


class BlacklistFilterWidget(UrlFilterWidget):
    def filterItems(self):
        return self._model.blacklist

    def itemAdded(self, url):
        filterItems = self.filterItems()
        dateStr = timeToString(filterItems[url][0] * 60)
        reasonStr = filterItems[url][1]

        tooltipStr = f"<b>{_('Date')}</b>: {dateStr}\n" \
                     f"<b>{_('Reason')}</b>: {reasonStr}"
        self._items[url].setToolTip(tooltipStr)

    def delete(self, url):
        UrlFilterWidget.delete(self, url)
        self._model.delInstanceFromBlacklist(url)
        self._model.apply()


class TimeoutFilterWidget(UrlFilterWidget):
    def filterItems(self):
        return self._model.timeoutList

    def itemAdded(self, url):
        # Add reason and duration tooltip
        durationStr = _("Until restart or manual removal.")
        filterItems = self.filterItems()
        timer = filterItems[url][0]
        if timer:
            durationStr = str(timer.interval() / 60000) + "min"
        reason = filterItems[url][1]
        tooltipStr = f"<b>{_('Reason')}</b>: \n{reason}\n<b>{_('Duration')}" \
                     f"</b>: {durationStr}"
        self._items[url].setToolTip(tooltipStr)

    def delete(self, url):
        UrlFilterWidget.delete(self, url)
        self._model.delInstanceFromTimeout(url)
        self._model.apply()


class NetworkFilterWidget(QWidget):
    def __init__(self, model, parent=None):
        """
        @type model: InstancesModel
        """
        QWidget.__init__(self, parent=parent)
        self._model = model
        layout = QHBoxLayout(self)

        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._webBox = QCheckBox("Web", self)
        self._torBox = QCheckBox("Tor", self)
        self._i2pBox = QCheckBox("i2p", self)

        self._webBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )
        self._torBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )
        self._i2pBox.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
        )

        layout.addWidget(self._webBox, 0, Qt.AlignLeft)
        layout.addWidget(self._torBox, 0, Qt.AlignLeft)
        layout.addWidget(self._i2pBox, 1, Qt.AlignLeft)

        self._model.changed.connect(self._modelChanged)

        self._webBox.stateChanged.connect(self.__changed)
        self._torBox.stateChanged.connect(self.__changed)
        self._i2pBox.stateChanged.connect(self.__changed)

    def __changed(self, state):
        self._model.changed.disconnect(self._modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self._modelChanged)

    def _modelChanged(self):
        self._webBox.stateChanged.disconnect(self.__changed)
        self._torBox.stateChanged.disconnect(self.__changed)
        self._i2pBox.stateChanged.disconnect(self.__changed)

        self._webBox.setChecked(False)
        self._torBox.setChecked(False)
        self._i2pBox.setChecked(False)

        filter = self._model.filter()

        if NetworkTypes.Web in filter['networkTypes']:
            self._webBox.setChecked(True)

        if NetworkTypes.Tor in filter['networkTypes']:
            self._torBox.setChecked(True)

        if NetworkTypes.I2P in filter['networkTypes']:
            self._i2pBox.setChecked(True)

        self._webBox.stateChanged.connect(self.__changed)
        self._torBox.stateChanged.connect(self.__changed)
        self._i2pBox.stateChanged.connect(self.__changed)

    def data(self):
        result = {
            'networkTypes': []  # Network Types to pass
        }

        if self._webBox.isChecked():
            result['networkTypes'].append(NetworkTypes.Web)

        if self._torBox.isChecked():
            result['networkTypes'].append(NetworkTypes.Tor)

        if self._i2pBox.isChecked():
            result['networkTypes'].append(NetworkTypes.I2P)

        return result


class VersionFilter(QWidget):
    def __init__(self, model, parent=None):
        """
        @type model: InstanceModelFilter
        """
        QWidget.__init__(self, parent=parent)
        self._model = model
        self._items = {}

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        minVerLayout = QHBoxLayout()
        self.minVersionCombo = QComboBox(self)
        self.minVersionCombo.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))
        minLabel = QLabel(_("Minimum:"), self)
        minLabel.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed))
        minVerLayout.addWidget(minLabel, 0, Qt.AlignLeft)
        minVerLayout.addWidget(self.minVersionCombo)

        self.invalidCheck = QCheckBox(_("Invalid"), self)
        self.invalidCheck.setToolTip(
            _("Include instances with a invalid version")
        )

        self.gitVersionCheck = QCheckBox(_("Development"), self)
        self.gitVersionCheck.setToolTip(
            _("Include development versions of SearX/SearXNG (git versions).")
        )

        self.dirtyVersionFlagCheck = QCheckBox(_("Dirty"), self)
        self.dirtyVersionFlagCheck.setToolTip(
            _("Include SearXNG git versions with uncommited changes.")
        )
        # Disable by default since there isn't a version selected by default.
        self.dirtyVersionFlagCheck.setEnabled(False)

        self.extraVersionFlagCheck = QCheckBox(_("Extra"), self)
        self.extraVersionFlagCheck.setToolTip(
            _("Include versions flagged as extra (IDK what that is? TODO).")
        )
        # Disable by default since there isn't a version selected by default.
        self.extraVersionFlagCheck.setEnabled(False)

        self.unknownVersionFlagCheck = QCheckBox(_("Unknown"), self)
        self.unknownVersionFlagCheck.setToolTip(
            _("Include Searx versions with unknown git commit.")
        )
        # Disable by default since there isn't a version selected by default.
        self.unknownVersionFlagCheck.setEnabled(False)

        # Add widgets to layout
        layout.addLayout(minVerLayout, 1)
        layout.addWidget(self.invalidCheck)
        layout.addWidget(self.gitVersionCheck)
        layout.addWidget(self.dirtyVersionFlagCheck)
        layout.addWidget(self.extraVersionFlagCheck)
        layout.addWidget(self.unknownVersionFlagCheck)

        # Signal connections
        self._model.changed.connect(self.__modelChanged)
        self.minVersionCombo.currentTextChanged.connect(
            self._minVersionComboChanged
        )
        self.invalidCheck.stateChanged.connect(self._invalidChanged)
        self.gitVersionCheck.stateChanged.connect(self._gitChanged)
        self.dirtyVersionFlagCheck.stateChanged.connect(self._dirtyChanged)
        self.extraVersionFlagCheck.stateChanged.connect(self._extraChanged)
        self.unknownVersionFlagCheck.stateChanged.connect(self._unknownChanged)

    def _minVersionComboChanged(self, versionStr):
        """ From the ComboBox
        """
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

    def _invalidChanged(self, state):
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

    def _gitChanged(self, state):
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

        enabled = bool(state)
        self.dirtyVersionFlagCheck.setEnabled(enabled)
        self.extraVersionFlagCheck.setEnabled(enabled)
        self.unknownVersionFlagCheck.setEnabled(enabled)

    def _dirtyChanged(self, state):
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

    def _extraChanged(self, state):
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

    def _unknownChanged(self, state):
        self._model.changed.disconnect(self.__modelChanged)
        self._model.updateKwargs(self.data())
        self._model.changed.connect(self.__modelChanged)

    def _compileVersions(self):
        """ Fill the combobox with available version strings
        """
        # Backup selected text so we can find the new index if it still exists.
        versionFilter = self._model.filter().get('version', {})
        selectedText = versionFilter.get('min', "")

        # Clear the combo
        self.minVersionCombo.clear()

        # Add the default None item
        self.minVersionCombo.addItem(_("Off"))

        # Unique
        uniqueVersions = []
        for url, instance in self._model.parentModel().items():
            version = instance.version
            partsString = version.partsString()
            if version.isValid() and partsString not in uniqueVersions:
                uniqueVersions.append(partsString)

        # Sort
        uniqueVersions.sort()

        # Add versions to combobox
        selectedIndex = 0
        for versionStr in uniqueVersions:
            if versionStr == selectedText:
                # The selected item is still there, store it's index so we can
                # set it back later.
                selectedIndex = self.minVersionCombo.count()
            self.minVersionCombo.addItem(versionStr)

        # Restore the selected index
        self.minVersionCombo.setCurrentIndex(selectedIndex)

    def __modelChanged(self):
        # Instances model has changed
        # Update versions combo
        self.minVersionCombo.currentTextChanged.disconnect(
            self._minVersionComboChanged
        )
        self._compileVersions()  # re-generate the combobox items
        self.minVersionCombo.currentTextChanged.connect(
            self._minVersionComboChanged
        )

        versionFilter = self._model.filter().get('version', {})
        self.gitVersionCheck.setChecked(versionFilter.get('git', True))
        self.dirtyVersionFlagCheck.setChecked(
            versionFilter.get('dirty', False)
        )
        self.extraVersionFlagCheck.setChecked(
            versionFilter.get('extra', False)
        )
        self.unknownVersionFlagCheck.setChecked(
            versionFilter.get('unknown', False)
        )

        self._compileVersions()

    def data(self):
        result = {
            'version': {
                'min': self.minVersionCombo.currentText(),
                'git': bool(self.gitVersionCheck.checkState()),
                'dirty': bool(self.dirtyVersionFlagCheck.checkState()),
                'extra': bool(self.extraVersionFlagCheck.checkState()),
                'unknown': bool(self.unknownVersionFlagCheck.checkState()),
                'invalid': bool(self.invalidCheck.checkState())
            }
        }
        return result


class CheckboxFilter(QCheckBox):
    def __init__(self, model, key, parent=None):
        """
        @type model: InstancesModel

        @param key: key to use (from model.filter())
        @type key: str
        """
        QCheckBox.__init__(self, parent=parent)
        self._model = model
        self._key = key

        self._model.changed.connect(self.__modelChanged)
        self.stateChanged.connect(self.__stateChanged)

    def __modelChanged(self):
        """ Model changed, update the checkbox.
        """
        filter_ = self._model.filter()
        state = filter_.get(self._key, True)
        self.stateChanged.disconnect(self.__stateChanged)
        self.setChecked(state)
        self.stateChanged.connect(self.__stateChanged)

    def __stateChanged(self):
        """ Checkbox state changed, update the model.
        """
        state = self.isChecked()
        self._model.updateKwargs({self._key: state})


class FilterWidget(QWidget):
    def __init__(self, model, parent=None):
        """
        @type model: searxqt.models.instances.InstanceModelFilter
        """
        QWidget.__init__(self, parent=parent)

        self._model = model

        layout = QFormLayout(self)
        layout.setLabelAlignment(
            Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
        )

        layout.addRow(QLabel(_("<h3>Filter</h3>"), self))

        self._networkFilter = NetworkFilterWidget(model, self)
        layout.addRow(_("Network") + ":", self._networkFilter)

        self._asnPrivacyFilter = None
        self._ipv6Filter = None
        self._analyticsFilter = None

        self._versionFilter = VersionFilter(model, self)
        layout.addRow(_("Version") + ":", self._versionFilter)

        self._urlFilterOut = BlacklistFilterWidget(model, self)
        layout.addRow(_("Blacklist") + ":", self._urlFilterOut)

        self._urlFilter = WhitelistFilterWidget(model, self)
        layout.addRow(_("Whitelist") + ":", self._urlFilter)

        self._tmpBlacklist = TimeoutFilterWidget(model, self)
        layout.addRow(_("Timeout") + ":", self._tmpBlacklist)

        model.parentModel().typeChanged.connect(self.__modelTypeChanged)

    def __modelTypeChanged(self, type_):
        layout = self.layout()
        if type_ == InstancesModelTypes.Stats2:
            if not self._asnPrivacyFilter:
                self._asnPrivacyFilter = CheckboxFilter(
                    self._model,
                    'asnPrivacy',
                    self
                )
                label = QLabel(_("Require ASN privacy") + ":")
                label.setWordWrap(True)
                label.setToolTip(_(
                    "Filter out instances that run their server at a known\n"
                    "malicious network like Google, CloudFlare, Akamai etc..\n"
                    "\n"
                    "This does not give any guarantee, it only filters known\n"
                    "privacy violators!"
                ))
                layout.insertRow(2, label, self._asnPrivacyFilter)

            if not self._ipv6Filter:
                self._ipv6Filter = CheckboxFilter(self._model, 'ipv6', self)
                label = QLabel(_("Require IPv6") + ":")
                label.setWordWrap(True)
                label.setToolTip(_(
                    "Only instances with at least one ipv6-address remain"
                    " if checked."
                ))
                layout.insertRow(3, label, self._ipv6Filter)

            if not self._analyticsFilter:
                self._analyticsFilter = CheckboxFilter(
                    self._model,
                    'analytics',
                    self
                )
                label = QLabel(_("Excl. analytics") + ":")
                label.setWordWrap(True)
                label.setToolTip(_(
                    "Exclude instances running tracking software."
                ))
                layout.insertRow(4, label, self._analyticsFilter)

        else:
            if self._asnPrivacyFilter:
                self._asnPrivacyFilter.deleteLater()
                layout.removeRow(2)
                self._asnPrivacyFilter = None

            if self._ipv6Filter:
                self._ipv6Filter.deleteLater()
                layout.removeRow(2)
                self._ipv6Filter = None

            if self._analyticsFilter:
                self._analyticsFilter.deleteLater()
                layout.removeRow(2)
                self._analyticsFilter = None


class InstancesStatsWidget(QWidget):
    def __init__(self, filterModel, parent=None):
        """
        @param filterModel:
        @type  filterModel: searxqt.models.instances.InstanceModelFilter
        """
        QWidget.__init__(self, parent=parent)

        self._filterModel = filterModel
        self._currentInstancesType = filterModel.parentModel().Type

        self._countLabel = None
        self._filterCountLabel = None
        self._lastUpdateLabel = None

        layout = QFormLayout(self)
        layout.setLabelAlignment(
            Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
        )

        self._toggleButton = Button("", self)
        layout.addRow(_("<h3>Stats</h3>"), self._toggleButton)

        self.__show()

        self._toggleButton.clicked.connect(self.__toggle)
        self._filterModel.parentModel().typeChanged.connect(
            self.__instancesModelTypeChanged
        )

    def collapsed(self):
        return bool(self._countLabel is None)

    def __toggle(self):
        if self.collapsed():
            self.__show()
        else:
            self.__hide()

    def __show(self):
        self._toggleButton.setText(_("Hide"))

        layout = self.layout()

        self._countLabel = QLabel("0", self)
        layout.addRow(_("Total count") + ":", self._countLabel)

        self._filterCountLabel = QLabel("0", self)
        layout.addRow(_("After filter count") + ":", self._filterCountLabel)

        self._lastUpdateLabel = QLabel("0", self)
        self._lastUpdateLabel.setWordWrap(True)
        layout.addRow(_("Last update") + ":", self._lastUpdateLabel)

        self.__modelChanged()  # Update count and after filter count
        self.__instancesModelTypeChanged(self._filterModel.parentModel().Type)

        self._filterModel.changed.connect(self.__modelChanged)

    def __hide(self):
        self._filterModel.changed.disconnect(self.__modelChanged)
        self.__instancesModelTypeChanged(InstancesModelTypes.NotDefined)

        self._toggleButton.setText(_("Show"))

        layout = self.layout()
        while layout.rowCount() > 1:
            layout.removeRow(1)

        self._countLabel = None
        self._filterCountLabel = None
        self._lastUpdateLabel = None

    """ InstancesType dependant methods below
    """

    def __instancesModelTypeChanged(self, type_):
        """ PersistentInstancesModel emitted changed.
        """
        if self.collapsed():
            return

        previousType = self._currentInstancesType
        self._currentInstancesType = type_

        # Disconnect signals
        if previousType == InstancesModelTypes.Stats2:
            self._filterModel.changed.disconnect(self.__updateLastUpdated)
        elif previousType == InstancesModelTypes.User:
            pass

        if type_ == InstancesModelTypes.Stats2:
            self._filterModel.changed.connect(self.__updateLastUpdated)
            self.__updateLastUpdated()
        else:
            self._lastUpdateLabel.setText("-")

    # InstancesModelTypes.Stats2
    def __updateLastUpdated(self):
        self._lastUpdateLabel.setText(
            timeToString(
                self._filterModel.parentModel().handler().lastUpdated()
            )
        )

    """ -------------------------------------------
    """

    def __modelChanged(self):
        self._countLabel.setText(str(len(self._filterModel.parentModel())))
        self._filterCountLabel.setText(str(len(self._filterModel)))
